package com.core.gwt.place.client;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

import com.google.gwt.i18n.client.DateTimeFormat;
import com.core.gwt.place.impl.Encoder;

/**
 * A builder and parser of place URL tokens which can render a set of name/value
 * pairs into a valid token or parse a token into a set of name/value pairs. <br>
 * <br>
 * To build a token, construct a blank PlaceToken object, use the different
 * value setter methods to store named data values into it and retrieve the
 * token by calling {@link #toString}. <br>
 * <br>
 * The resulting token is a sequence of name=value pairs separated by &: e.g.
 * 'name1=value1&name2=value2'. <br>
 * <br>
 * Invalid characters in the names or values - e.g. spaces, tabs, carriage
 * returns, ampersands, equal signs, etc. - will be automatically substituted
 * with escaped valid character sequences. <br>
 * <br>
 * To parse a token, construct a new PlaceToken object, passing the token as a
 * constructor argument, and then use the different value getter methods to
 * retrieve the named data values.
 * 

 */
public class PlaceToken {
	private static final Character PAIR_DELIMITER = '&';
	private static final Character PART_DELIMITER = '=';
	private static String DATE_FORMAT_PATTERN = "yyyyMMddHHmmss";
	private static String[][] ENCODING = { { "_", "__" }, { ":", "_c" },
			{ "=", "_e" }, { "&", "_a" }, { " ", "_s" }, { "\t", "_t" },
			{ "\r", "_r" }, { "\n", "_n" }, { "?", "_q" } };

	private final Map<String, String> values;

	/**
	 * Initializes a blank PlaceToken.
	 */
	public PlaceToken() {
		this(null);
	}

	/**
	 * Parses the token argument and initializes a PlaceToken with the data from
	 * the argument.
	 */
	public PlaceToken(final String token) {
		values = new LinkedHashMap<String, String>();
		if (null != token) {
			final String pairSplitRegex = "\\" + PAIR_DELIMITER;
			final String[] pairs = token.split(pairSplitRegex);
			for (final String pair : pairs) {
				final String partSplitRegex = "\\" + PART_DELIMITER;
				final String[] parts = pair.split(partSplitRegex, 2);
				if (2 == parts.length) {
					values.put(decode(parts[0]), decode(parts[1]));
				}
			}
		}
	}

	/**
	 * Returns the boolean data value associated with the specified name or
	 * <code>false</code> if the named value is not present.
	 */
	public boolean getBoolean(final String name) {
		return getBoolean(name, false);
	}

	/**
	 * Returns the boolean data value associated with the specified name or
	 * <code>defaultValue</code> if the named value is not present.
	 */
	public boolean getBoolean(final String name, final boolean defaultValue) {
		boolean result = defaultValue;
		if (values.containsKey(name)) {
			final String value = values.get(name);
			if (value != null) {
				result = Boolean.parseBoolean(value);
			}
		}
		return result;
	}

	/**
	 * Returns the {@link Date} data value associated with the specified name or
	 * <code>null</code> if the named value is not present.
	 */
	public Date getDate(final String name) {
		return getDate(name, null);
	}

	/**
	 * Returns the {@link Date} data value associated with the specified name or
	 * <code>defaultValue</code> if the named value is not present.
	 */
	public Date getDate(final String name, final Date defaultValue) {
		Date result = defaultValue;
		if (values.containsKey(name)) {
			result = parseDate(values.get(name), defaultValue);
		}
		return result;
	}

	/**
	 * Returns the double data value associated with the specified name or
	 * <code>0</code> if the named value is not present.
	 */
	public double getDouble(final String name) {
		return getDouble(name, 0);
	}

	/**
	 * Returns the double data value associated with the specified name or
	 * <code>defaultValue</code> if the named value is not present.
	 */
	public double getDouble(final String name, final double defaultValue) {
		double result = defaultValue;
		if (values.containsKey(name)) {
			final String value = values.get(name);
			if (value != null) {
				try {
					result = Double.parseDouble(value);
				} catch (final NumberFormatException e) {
				}
			}
		}
		return result;
	}

	/**
	 * Returns the int data value associated with the specified name or
	 * <code>0</code> if the named value is not present.
	 */
	public int getInt(final String name) {
		return getInt(name, 0);
	}

	/**
	 * Returns the int data value associated with the specified name or
	 * <code>defaultValue</code> if the named value is not present.
	 */
	public int getInt(final String name, final int defaultValue) {
		int result = defaultValue;
		if (values.containsKey(name)) {
			final String value = values.get(name);
			if (value != null) {
				try {
					result = Integer.parseInt(value);
				} catch (final NumberFormatException e) {
				}
			}
		}
		return result;
	}

	/**
	 * Returns the long data value associated with the specified name or
	 * <code>0</code> if the named value is not present.
	 */
	public long getLong(final String name) {
		return getLong(name, 0);
	}

	/**
	 * Returns the long data value associated with the specified name or
	 * <code>defaultValue</code> if the named value is not present.
	 */
	public long getLong(final String name, final long defaultValue) {
		long result = defaultValue;
		if (values.containsKey(name)) {
			final String value = values.get(name);
			if (value != null) {
				try {
					result = Long.parseLong(value);
				} catch (final NumberFormatException e) {
				}
			}
		}
		return result;
	}

	/**
	 * Returns the string data value associated with the specified name or
	 * <code>null</code> if the named value is not present.
	 */
	public String getString(final String name) {
		return getString(name, null);
	}

	/**
	 * Returns the string data value associated with the specified name or
	 * <code>defaultValue</code> if the named value is not present.
	 */
	public String getString(final String name, final String defaultValue) {
		String result = defaultValue;
		if (values.containsKey(name)) {
			result = values.get(name);
		}
		return result;
	}

	/**
	 * Stores a boolean data value - if different from <code>false</code> -
	 * associated with the specified name.
	 */
	public void setBoolean(final String name, final boolean value) {
		setBoolean(name, value, false);
	}

	/**
	 * Stores a boolean data value - if different from <code>defaultValue</code>
	 * - associated with the specified name.
	 */
	public void setBoolean(final String name, final boolean value,
			final boolean defaultValue) {
		values.remove(name);
		if (value != defaultValue) {
			values.put(name, String.valueOf(value));
		}
	}

	/**
	 * Stores a {@link Date} data value - if different from <code>null</code> -
	 * associated with the specified name.
	 */
	public void setDate(final String name, final Date value) {
		setDate(name, value, null);
	}

	/**
	 * Stores a {@link Date} data value - if different from
	 * <code>defaultValue</code> - associated with the specified name.
	 */
	public void setDate(final String name, final Date value,
			final Date defaultValue) {
		values.remove(name);
		if (value != defaultValue) {
			values.put(name, formatDate(value));
		}
	}

	/**
	 * Stores a double data value - if different from <code>0</code> -
	 * associated with the specified name.
	 */
	public void setDouble(final String name, final double value) {
		setDouble(name, value, 0);
	}

	/**
	 * Stores a double data value - if different from <code>defaultValue</code>
	 * - associated with the specified name.
	 */
	public void setDouble(final String name, final double value,
			final double defaultValue) {
		values.remove(name);
		if (value != defaultValue) {
			values.put(name, String.valueOf(value));
		}
	}

	/**
	 * Stores an int data value - if different from <code>0</code> - associated
	 * with the specified name.
	 */
	public void setInt(final String name, final int value) {
		setInt(name, value, 0);
	}

	/**
	 * Stores an int data value - if different from <code>defaultValue</code> -
	 * associated with the specified name.
	 */
	public void setInt(final String name, final int value,
			final int defaultValue) {
		values.remove(name);
		if (value != defaultValue) {
			values.put(name, String.valueOf(value));
		}
	}

	/**
	 * Stores a long data value - if different from <code>0</code> - associated
	 * with the specified name.
	 */
	public void setLong(final String name, final long value) {
		setLong(name, value, 0);
	}

	/**
	 * Stores a long data value - if different from <code>defaultValue</code> -
	 * associated with the specified name.
	 */
	public void setLong(final String name, final long value,
			final long defaultValue) {
		values.remove(name);
		if (value != defaultValue) {
			values.put(name, String.valueOf(value));
		}
	}

	/**
	 * Stores a string data value - if different from <code>null</code> -
	 * associated with the specified name.
	 */
	public void setString(final String name, final String value) {
		setString(name, value, null);
	}

	/**
	 * Stores a string data value - if different from <code>defaultValue</code>
	 * - associated with the specified name.
	 */
	public void setString(final String name, final String value,
			final String defaultValue) {
		values.remove(name);
		if (value != defaultValue) {
			values.put(name, value);
		}
	}

	/**
	 * Returns the properly formatted place token representing the data in this
	 * object.
	 */
	@Override
	public String toString() {
		final StringBuilder buffer = new StringBuilder();
		for (final String name : values.keySet()) {
			final String value = values.get(name);
			if (null != value) {
				if (value.length() > 0) {
					if (buffer.length() > 0) {
						buffer.append(PAIR_DELIMITER);
					}
					buffer.append(encode(name));
					buffer.append(PART_DELIMITER);
					buffer.append(encode(value));
				}
			}
		}
		return buffer.toString();
	}

	/**
	 * Decodes a string encoded with {@link #encode}.
	 */
	private String decode(final String value) {
		return Encoder.decode(value, ENCODING);
	}

	/**
	 * Encodes a string, replacing all illegal characters with escaped
	 * representations.
	 */
	private String encode(final String value) {
		return Encoder.encode(value, ENCODING);
	}

	/**
	 * Formats a date value as a string.
	 */
	private String formatDate(final Date value) {
		String result = null;
		if (value != null) {
			result = DateTimeFormat.getFormat(DATE_FORMAT_PATTERN)
					.format(value);
		}
		return result;
	}

	/**
	 * Parses a date string formatted by {@link #formatDate} and returns a
	 * {@link Date} or <code>defaultValue</code> if the string does not
	 * represent a valid date.
	 */
	private Date parseDate(final String value, final Date defaultValue) {
		Date result = defaultValue;
		if (value != null) {
			if (value.length() == DATE_FORMAT_PATTERN.length()) {
				result = DateTimeFormat.getFormat(DATE_FORMAT_PATTERN).parse(
						value);
			}
		}
		return result;
	}

}
